//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Collections.Generic;
using EffiProzDB.Lib;

namespace EffiProzDB
{
    /**
     * Contains a set of Grantee objects, and supports operations for creating,
     * finding, modifying and deleting Grantee objects for a Database; plus
     * Administrative privileges.
     *
     * @author boucherb@users
     * @author fredt@users
     * @author unsaved@users
     *
     * @version 1.8.0
     * @since 1.8.0
     * @see Grantee
     */
    public class GranteeManager : GrantConstants
    {

        /**
         * The role name reserved for authorization of INFORMATION_SCHEMA and
         * system objects.
         */
        public const string SYSTEM_AUTHORIZATION_NAME = "_SYSTEM";

        /** The role name reserved for ADMIN users. */
        public const string DBA_ADMIN_ROLE_NAME = "DBA";

        /** The role name reserved for the special PUBLIC pseudo-user. */
        public const string PUBLIC_ROLE_NAME = "PUBLIC";

        /**
         * An empty list that is returned from
         * {@link #listTablePrivileges listTablePrivileges} when
         * it is detected that neither this <code>User</code> object or
         * its <code>PUBLIC</code> <code>User</code> object attribute have been
         * granted any rights on the <code>Table</code> object identified by
         * the specified <code>HsqlName</code> object.
         *
         */
        public static String[] emptyRightsList = new String[0];

        /**
         * MAP:  int => HsqlArrayList. <p>
         *
         * This map caches the lists of <code>String</code> objects naming the rights
         * corresponding to each valid set of rights flags, as returned by
         * {@link #listRightNames listRightNames}
         *
         */
        public static Dictionary<int, string[]> hRightsLists = new Dictionary<int, string[]>();

        /**
         * Used to provide access to the RoleManager for Grantee.isAccessible()
         * lookups
         */
        /*
         * Our map here has the same keys as the UserManager map
         * EXCEPT that we include all roles, including the SYSTEM_AUTHORIZATION_NAME
         * because we need o keep track of those permissions, but not his identity.
         * I.e., our list here is all-inclusive, whether the User or Role is
         * visible to database users or not.
         */

        /**
         * Map of String-to-Grantee-objects.<p>
         * Primary object maintained by this class
         */
        private SortedDictionary<string, Grantee> map = new SortedDictionary<string, Grantee>();

        /**
         * This object's set of Role objects. <p>
         * role-Strings-to-Grantee-object
         */
        private SortedDictionary<string, Grantee> roleMap = new SortedDictionary<string, Grantee>();

        /**
         * Construct the GranteeManager for a Database.
         *
         * Construct special Grantee objects for PUBLIC and SYS, and add them
         * to the Grantee map.
         * We depend on the corresponding User accounts being created
         * independently so as to remove a dependency to the UserManager class.
         *
         * @param inDatabase Only needed to link to the RoleManager later on.
         */
        public GranteeManager(Database inDatabase)
        {
            addRole(GranteeManager.DBA_ADMIN_ROLE_NAME);
            getRole(GranteeManager.DBA_ADMIN_ROLE_NAME).setAdminDirect();
        }

        public static Dictionary<string, int> rightsStringLookup = new Dictionary<string, int>(7);

         static GranteeManager()
        {
            rightsStringLookup.Add(GrantConstants.S_R_ALL, GrantConstants.ALL);
            rightsStringLookup.Add(GrantConstants.S_R_SELECT, GrantConstants.SELECT);
            rightsStringLookup.Add(GrantConstants.S_R_UPDATE, GrantConstants.UPDATE);
            rightsStringLookup.Add(GrantConstants.S_R_DELETE, GrantConstants.DELETE);
            rightsStringLookup.Add(GrantConstants.S_R_INSERT, GrantConstants.INSERT);
        }

        /**
         * Grants the rights represented by the rights argument on
         * the database object identified by the dbobject argument
         * to the Grantee object identified by name argument.<p>
         *
         *  Note: For the dbobject argument, Java Class objects are identified
         *  using a string object whose value is the fully qualified name
         *  of the Class, while Table and other objects are
         *  identified by an HsqlNameManager.HsqlName object.  A Table
         *  object identifier must be precisely the one obtained by calling
         *  table.getName(); if a different HsqlName
         *  object with an identical name attribute is specified, then
         *  rights checks and tests will fail, since the HsqlName
         *  class : its {@link HsqlName#hashCode hashCode} and
         *  {@link HsqlName#equals equals} methods based on pure object
         *  identity, rather than on attribute values. <p>
         */
        public void grant(string name, Object dbobject,
                   int rights)
        {

            Grantee g = get(name);

            if (g == null)
            {
                throw Trace.error(Trace.NO_SUCH_GRANTEE, name);
            }

            if (isImmutable(name))
            {
                throw Trace.error(Trace.NONMOD_GRANTEE, name);
            }

            g.grant(dbobject, rights);
            g.updateAllRights();

            if (g.isRole)
            {
                updateAllRights(g);
            }
        }

        /**
         * Grant a role to this Grantee.
         */
        public void grant(string name, string role)
        {

            Grantee grantee = get(name);

            if (grantee == null)
            {
                throw Trace.error(Trace.NO_SUCH_GRANTEE, name);
            }

            if (isImmutable(name))
            {
                throw Trace.error(Trace.NONMOD_GRANTEE, name);
            }

            Grantee r = get(role);

            if (r == null)
            {
                throw Trace.error(Trace.NO_SUCH_ROLE, role);
            }

            if (role.Equals(name))
            {
                throw Trace.error(Trace.CIRCULAR_GRANT, name);
            }

            // boucherb@users 20050515
            // SQL 2003 Foundation, 4.34.3
            // No cycles of role grants are allowed.
            if (r.hasRole(name))
            {

                // boucherb@users
                // TODO: Correct reporting of actual grant path
                throw Trace.error(Trace.CIRCULAR_GRANT,
                                  Trace.getMessage(Trace.ALREADY_HAVE_ROLE)
                                  + " GRANT " + name + " TO " + role);
            }

            if (grantee.getDirectRoles().Contains(role))
            {
                throw Trace.error(Trace.ALREADY_HAVE_ROLE, role);
            }

            grantee.grant(role);
            grantee.updateAllRights();

            if (grantee.isRole)
            {
                updateAllRights(grantee);
            }
        }

        /**
         * Revoke a role from a Grantee
         */
        public void revoke(string name, string role)
        {

            Grantee g = get(name);

            if (g == null)
            {
                throw Trace.error(Trace.NO_SUCH_GRANTEE, name);
            }

            g.revoke(role);
            g.updateAllRights();

            if (g.isRole)
            {
                updateAllRights(g);
            }
        }

        /**
         * Revokes the rights represented by the rights argument on
         * the database object identified by the dbobject argument
         * from the User object identified by the name
         * argument.<p>
         * @see #grant
         */
        public void revoke(string name, Object dbobject,
                    int rights)
        {

            Grantee g = get(name);

            g.revoke(dbobject, rights);
            g.updateAllRights();

            if (g.isRole)
            {
                updateAllRights(g);
            }
        }

        /**
         * Removes a role without any privileges from all grantees
         */
        public void removeEmptyRole(Grantee role)
        {

            string name = role.getName();

            foreach (var grantee in map.Values)
            {           

                grantee.roles.Remove(name);
            }
        }

        /**
         * Removes all rights mappings for the database object identified by
         * the dbobject argument from all Grantee objects _in the set.
         */
        public void removeDbObject(Object dbobject)
        {

            foreach (var grantee in map.Values)
            {

                grantee.revokeDbObject(dbobject);
            }
        }

        /**
         * First updates all ROLE Grantee objects. Then updates all USER Grantee
         * Objects.
         */
        public void updateAllRights(Grantee role)
        {

            string name = role.getName();

            foreach (var grantee in map.Values)
            {            

                if (grantee.isRole)
                {
                    grantee.updateNestedRoles(name);
                }
            }

            foreach (var grantee in map.Values)
            {       

                if (!grantee.isRole)
                {
                    grantee.updateAllRights();
                }
            }
        }

        /**
         */
        public bool removeGrantee(string name)
        {

            /*
             * Explicitly can't remove PUBLIC_USER_NAME and system grantees.
             */
            if (isReserved(name))
            {
                return false;
            }

            Grantee g = null;
            try
            {
                g = map[name];
            }
            catch (Exception) { }

            map.Remove(name);

            if (g == null)
            {
                return false;
            }



            g.clearPrivileges();
            updateAllRights(g);

            if (g.isRole)
            {
                roleMap.Remove(name);
                removeEmptyRole(g);
            }

            return true;
        }

        /**
         * We don't have to worry about anything manually creating a reserved
         * account, because the reserved accounts are created upon DB
         * initialization.  If somebody tries to create one of these accounts
         * after that, it will fail because the account will already exist.
         * (We do prevent them from being removed, elsewhere!)
         */
        public Grantee addGrantee(string name)
        {

            if (map.ContainsKey(name))
            {
                throw Trace.error(Trace.GRANTEE_ALREADY_EXISTS, name);
            }

            Grantee pubGrantee = null;

            if (!isReserved(name))
            {
                pubGrantee = get(PUBLIC_ROLE_NAME);

                if (pubGrantee == null)
                {
                    Trace.doAssert(
                        false, Trace.getMessage(Trace.MISSING_PUBLIC_GRANTEE));
                }
            }

            Grantee g = new Grantee(name, pubGrantee, this);

            map.Add(name, g);

            return g;
        }

        /**
         * Returns true if named Grantee object exists.
         * This will return true for reserved Grantees
         * SYSTEM_AUTHORIZATION_NAME, ADMIN_ROLE_NAME, PUBLIC_USER_NAME.
         */
        public bool isGrantee(string name)
        {
            return (map.ContainsKey(name));
        }

        public static int getCheckRight(string right)
        {

            int r = getRight(right);

            if (r != 0)
            {
                return r;
            }

            throw Trace.error(Trace.NO_SUCH_RIGHT, right);
        }

        /**
         * Translate a string representation or right(s) into its numeric form.
         */
        static int getRight(string right)
        {
            int iright = 0;
            rightsStringLookup.TryGetValue(right, out iright);

            return iright;
        }

        /**
         * Returns a comma separated list of right names corresponding to the
         * right flags set _in the right argument. <p>
         */
        public static string getRightsList(int rights)
        {

            //        checkValidFlags(right);
            if (rights == 0)
            {
                return null;
            }

            if (rights == GrantConstants.ALL)
            {
                return GrantConstants.S_R_ALL;
            }

            return StringUtil.getList(getRightsArray(rights), ",", "");
        }

        /**
         * Retrieves the list of right names represented by the right flags
         * set _in the specified <code>Integer</code> object's <code>int</code>
         * value. <p>
         *
         * @param rights An int representing a set of right flags
         * @return an empty list if the specified <code>Integer</code> object is
         *        null, else a list of rights, as <code>String</code> objects,
         *        represented by the rights flag bits set _in the specified
         *        <code>Integer</code> object's int value.
         *
         */
        public static String[] getRightsArray(int rights)
        {

            if (rights == 0)
            {
                return emptyRightsList;
            }

            String[] list = null;
            hRightsLists.TryGetValue(rights, out list);

            if (list != null)
            {
                return list;
            }

            list = getRightsArraySub(rights);

            hRightsLists.Add(rights, list);

            return list;
        }

        private static String[] getRightsArraySub(int right)
        {

            //        checkValidFlags(right);
            if (right == 0)
            {
                return emptyRightsList;
            }

            List<string> a = new List<string>();           

            foreach (var _rightstring in  rightsStringLookup.Keys)
            {               

                if (_rightstring.Equals(GrantConstants.S_R_ALL))
                {
                    continue;
                }

                int i = 0;                
                rightsStringLookup.TryGetValue(_rightstring, out i);

                if ((right & i) != 0)
                {
                    a.Add(_rightstring);
                }
            }

            return a.ToArray();
        }

        /**
         * Retrieves the set of distinct, fully qualified Java <code>Class</code>
         * names upon which any grants currently exist to elements in
         * this collection. <p>
         * @return the set of distinct, fully qualified Java Class names, as
         *        <code>String</code> objects, upon which grants currently exist
         *        to the elements of this collection
         *
         */
        public HashSet<string> getGrantedClassNames()
        {
             
            HashSet<string> _out;       

         
            _out = new HashSet<string>();

            foreach (var grantee in map.Values)
            {             

                if (grantee == null)
                {
                    continue;
                }

                var e = grantee.getGrantedClassNames(false);

                foreach (var ei in e)
                {
                    _out.Add(ei);
                }
            }

            return _out;
        }

        public Grantee get(string name)
        {
            Grantee g = null;
            map.TryGetValue(name, out g);
            return g;
        }
 
        public IEnumerable<Grantee>  getGrantees()
        {
            return map.Values;
        }

        public static bool validRightString(string rightString)
        {
            return getRight(rightString) != 0;
        }

        public static bool isImmutable(string name)
        {
            return name.Equals(SYSTEM_AUTHORIZATION_NAME)
                   || name.Equals(DBA_ADMIN_ROLE_NAME);
        }

        public static bool isReserved(string name)
        {

            return name.Equals(SYSTEM_AUTHORIZATION_NAME)
                   || name.Equals(DBA_ADMIN_ROLE_NAME)
                   || name.Equals(PUBLIC_ROLE_NAME);
        }

        /**
         * Creates a new Role object under management of this object. <p>
         *
         *  A set of constraints regarding user creation is imposed: <p>
         *
         *  <OL>
         *    <LI>Can't create a role with name same as any right.
         *
         *    <LI>If the specified name is null, then an
         *        ASSERTION_FAILED exception is thrown stating that
         *        the name is null.
         *
         *    <LI>If this object's collection already contains an element whose
         *        name attribute equals the name argument, then
         *        a GRANTEE_ALREADY_EXISTS or ROLE_ALREADY_EXISTS Trace
         *        is thrown.
         *        (This will catch attempts to create Reserved grantee names).
         *  </OL>
         */
        public string addRole(string name)
        {

            /*
             * Role names can't be right names because that would cause
             * conflicts with "GRANT name TO...".  This doesn't apply to
             * User names or Grantee names _in general, since you can't
             * "GRANT username TO...".  That's why this check is only here.
             */
            if (name == null)
            {
                Trace.doAssert(false, Trace.getMessage(Trace.NULL_NAME));
            }

            Grantee g = null;

            if (GranteeManager.validRightString(name))
            {
                throw Trace.error(Trace.ILLEGAL_ROLE_NAME, name);
            }

            g = addGrantee(name);
            g.isRole = true;

      

            if (roleMap.ContainsKey(name))
            {
                throw Trace.error(Trace.ROLE_ALREADY_EXISTS, name);
            }
            else
            {
                roleMap.Add(name, g);
            }

            // I don't think can get this trace since every roleMap element
            // will have a Grantee element which was already verified
            // above.  Easier to leave this check here than research it.
            return name;
        }

        /**
         * Attempts to drop a Role with the specified name
         *  from this object's set. <p>
         *
         *  A successful drop action consists of: <p>
         *
         *  <UL>
         *
         *    <LI>removing the Grantee object with the specified name
         *        from the set.
         *
         *    <LI>revoking all rights from the removed object<br>
         *        (this ensures that _in case there are still references to the
         *        just dropped Grantee object, those references
         *        cannot be used to erronously access database objects).
         *
         *  </UL> <p>
         *
         */
       public void dropRole(string name)
        {

            if (name.Equals(GranteeManager.DBA_ADMIN_ROLE_NAME))
            {
                throw Trace.error(Trace.ACCESS_IS_DENIED);
            }

            if (!isRole(name))
            {
                throw Trace.error(Trace.NO_SUCH_ROLE, name);
            }

            removeGrantee(name);
            roleMap.Remove(name);
        }

        public IEnumerable<string> getRoleNames()
        {
            return roleMap.Keys;
        }

        /**
         * Returns Grantee for the named Role
         */
        public Grantee getRole(string name)
        {

            if (!isRole(name))
            {
                Trace.doAssert(false, "No role '" + name + "'");
            }

            Grantee g = null;
            roleMap.TryGetValue(name,out g);

            if (g == null)
            {
                throw Trace.error(Trace.MISSING_GRANTEE, name);
            }

            return g;
        }

        public bool isRole(string name)
        {
            return roleMap.ContainsKey(name);
        }
    }
}
